package org.genuflex.utils
{
	import flash.utils.Dictionary;
	import mx.collections.ArrayCollection;
	import mx.utils.ObjectUtil;
	import org.genuflex.vo.GenuflexMetadata;
	import org.genuflex.vo.GenuflexAssociation;
	import org.genuflex.vo.BelongsToAssociation;
	import org.genuflex.vo.HasManyAssociation;
	import org.genuflex.vo.HasAndBelongsToManyAssociation;
	
	/*
	 * GenuflexDataCache provides a centralized source for all VOs 
	 *  Its primary purpose is to allow for bindings scattered throughout
	 *  the application can be kept and updated when one instance of data
	 *  comes back from the server and represents a different instance of
	 *  data already client-side
	 */
	public class GenuflexDataCache
	{	
		
		private static var dataCache : GenuflexDataCache;
		
		/*
		 * dataDictionary
		 *
		 *  Contains all data with a key of the Class name + id which 
		 *  is assumed to be unique system-wide
		 */		
		private var dataDictionary : Dictionary;
		
		/*
		 * assocBySource
		 *
		 *  A dictionary of ArrayCollections with the 
		 *  associations in the system
		 */		
		private var assocBySource : Dictionary;
		private var	assocByDestination : Dictionary;
		
		/*
		 * managedClasses
		 *  
		 *  Keeps track of any class for which this cache contains
		 *  data
		 */
		public var managedClasses : ArrayCollection;
		
		public static function getInstance() : GenuflexDataCache {
	    	if ( dataCache == null ) {
	      		dataCache = new GenuflexDataCache();
	      	}
		
			return dataCache;
		}
	      
		public function GenuflexDataCache() {	
			if ( dataCache != null ) {
				throw new Error( "Only one GenuflexDataCache instance should be instantiated" );	
			}		
			
			dataDictionary = new Dictionary();
			assocBySource = new Dictionary();
			assocByDestination = new Dictionary();
			managedClasses = new ArrayCollection();
		}
		
		public function contains(klass : Class, id : int) : Boolean {
			return dataDictionary[klass] != null && dataDictionary[klass][id] != null;
		}
		public function getData(klass : Class, id : int) : Object {
			var retObj : Object = null;
			
			if (dataDictionary[klass] != null) {
				retObj = dataDictionary[klass][id];
			}
			
			return retObj;
		}
		public function setData(klass : Class, value : Object, idField : String = null) : void {
			//If the dictionary already has this value, 
			// we need to update the same instance with the value instance's
			// values
			
			//If value is null, we're outta here
			if (value == null) {
				return;
			}
			
			var id : int = 0;
			
			if (idField == null) {
				id = value.id;
			}
			else {
				id = value[idField];
			}

			if (contains(klass, id)) {
				performShallowCopy(value, getData(klass,id));
			}
			//Just pop it into the dictionary
			else {
				if (dataDictionary[klass] == null) {
					dataDictionary[klass] = new Dictionary();
					addToManagedClasses(klass);		
				}
				
				dataDictionary[klass][id] = value;
				
			}
		}
		public function removeData(klass : Class, value : Object, idField : String = null) : void {

			//If value is null, we're outta here
			if (value == null) {
				return;
			}
			
			var id : int = 0;
			
			if (idField == null) {
				id = value.id;
			}
			else {
				id = value[idField];
			}

			if (contains(klass, id)) {
				delete dataDictionary[klass][id];
			}
		}
		public function updateData(klass : Class, value : Object, idField : String = null) : Object {

			//If value is null, we're outta here
			if (value == null) {
				return null;
			}

			//Update or add the instance that now lives in the dictionary
			setData(klass, value, idField);

			var id : int = 0;
			
			if (idField == null) {
				id = value.id;
			}
			else {
				id = value[idField];
			}
			//Return the instance that now lives in the dictionary
			return getData(klass, id);
			
		}
		
		/*
		 * addToManagedClasses
		 *
		 *  Keep track of the classes in the cache
		 */
		private function addToManagedClasses(klass : Class) : void {
			if (!managedClasses.contains(klass)) {
				managedClasses.addItem(klass);
			}
		}
		
		/*
		 * addAssociation
		 *
		 *  Keep track of the classes in the cache
		 *  Why is the key for assocBySource destClass? Because
		 *   when we're coming back into update a class that's how we'll pull the
		 *   association
		 */
		public function addAssociation(association : GenuflexAssociation) : void {
			if (assocBySource[association.destClass] == null) {
				assocBySource[association.destClass] = new ArrayCollection();
			}
			
			ArrayCollection(assocBySource[association.destClass]).addItem(association);

			if (assocByDestination[association.sourceClass] == null) {
				assocByDestination[association.sourceClass] = new ArrayCollection();
			}
			
			ArrayCollection(assocByDestination[association.sourceClass]).addItem(association);
		}
		
		public function belongsToAssociations(sourceClass : Class) : ArrayCollection {
			return getAssociationsByType(sourceClass, BelongsToAssociation);
		}
		public function hasManyAssociations(sourceClass : Class) : ArrayCollection {
			return getAssociationsByType(sourceClass, HasManyAssociation);
		}
		public function hasAndBelongsToManyAssociations(sourceClass : Class) : ArrayCollection {
			return getAssociationsByType(sourceClass, HasAndBelongsToManyAssociation);
		}
		public function getBelongsToByDestination(destClass : Class, name : String) : BelongsToAssociation {
			var destAssoc : ArrayCollection = null;
			var belongsTo : BelongsToAssociation = null;
			
			if (GenuflexDataFunctions.isNotEmpty(assocByDestination[destClass])) {
				//Run through all associations grabbing the matching ones by type
				for each(var association : GenuflexAssociation in assocByDestination[destClass]) {
					if (association is BelongsToAssociation && BelongsToAssociation(association).name == name) {
						belongsTo = association as BelongsToAssociation;
					}
				}
			}
			
			return belongsTo;
		}
		
		private function getAssociationsByType(sourceClass : Class, associationType : Class) : ArrayCollection {
			var retArray : ArrayCollection = new ArrayCollection();
			
			if (GenuflexDataFunctions.isNotEmpty(assocBySource[sourceClass])) {
				
				//Run through all associations grabbing the matching ones by type
				for each(var association : GenuflexAssociation in assocBySource[sourceClass]) {
					if (association is associationType) {
						retArray.addItem(association);
					}
				}
			}
			
			return retArray;
		}
		
		
		/*
		 * updateHasManyData
		 *
		 *  Based on the basic belongs_to, has_many and has_and_belongs_to_many relationships,
		 * update field_name data in the cache on each klass1 object found. 
		 * in the following example:
		 * 
		 * EX: model.dataCache.updateHasManyData(AuthorVO, "blog_posts", BlogPostVO, "author_id");
		 *
		 * all AuthorVO records are found and for each one, blog_posts is assigned with
		 * all of the BlogPostVO records in the cache with a matching author_id
		 */
		 public function updateHasManyData(destinationClass : Class, destinationField : String, sourceClass : Class, foreignKeyField : String) : void {
			if (!managedClasses.contains(destinationClass) || !managedClasses.contains(sourceClass) ) {
				//The destination or source class isn't in the cache
				return;
			}
			
			//For each object in the cache of class = destinationClass,
			// update the variable defined by destinationField to the correct values in the cache
			for each(var obj : Object in dataDictionary[destinationClass]) {				
				obj[destinationField] = GenuflexDataFunctions.listByValue(dataDictionary[sourceClass], obj.id, foreignKeyField).toArray();
			}
		 }
		 		 
		 public function updateBelongsToData(destinationClass : Class, destinationField : String, sourceClass : Class, foreignKeyField : String) : void {
		 	
		 	var tmpObj : Object = null;
		 	
			if (!managedClasses.contains(destinationClass) || !managedClasses.contains(sourceClass) ) {
				//The destination or source class isn't in the cache
				return;
			}
			
			//For each object in the cache of class = destinationClass,
			// update the variable defined by destinationField to the correct values in the cache
			for each(var obj : Object in dataDictionary[destinationClass]) {				
				try {					
					tmpObj = GenuflexDataFunctions.findByID(dataDictionary[sourceClass], obj[foreignKeyField]);
					
					//Update only if the cache has a match
					if (tmpObj != null) {
						obj[destinationField] = tmpObj;
					}					
				}
				catch(e : Error) {
					GenuflexLogger.Error("Error in updateBelongsToData: dest: "+destinationClass+"."+destinationField+" source: "+sourceClass +"."+foreignKeyField+":"+ e.message);
				}
			}
		 }		 
		
		 		 
		 public function updateHasAndBelongsToManyData(destinationClass : Class, destinationField : String, sourceClass : Class, sourceField : String) : void {
			if (!managedClasses.contains(destinationClass) || !managedClasses.contains(sourceClass) ) {
				//The destination or source class isn't in the cache
				return;
			}
			
			//For each object in the cache of class = destinationClass,
			// update the variable defined by destinationField to the correct values in the cache
			for each(var destObj : Object in dataDictionary[destinationClass]) {				
				var matches : ArrayCollection = new ArrayCollection();

				if (dataDictionary[sourceClass] != null) {
					for each(var sourceObj : Object in dataDictionary[sourceClass]) {
						//If the inner array of data includes a matching object, add
						// obj to matches
						var match : Object = GenuflexDataFunctions.findByID(sourceObj[sourceField], destObj.id);
	
						if (match != null) {
							matches.addItem(sourceObj);
						}
					}
				}

				destObj[destinationField] = matches.toArray();
			}
		 }				
		public function getAllObjects(klass : Class) : Array {
			var retArray : Array = new Array();
			
			if (dataDictionary[klass] != null) {
				for each(var obj : Object in dataDictionary[klass]) {
					retArray.push(obj);
				}
			}
			
			return retArray;
		}
		 
		/*
		 * performShallowCopy
		 *
		 *  In order to maintain the integrity of all bindings to the instance of dest,
		 *  but also to update values, take the values from source and move them to dest
		 */
		private function performShallowCopy(source : Object, dest : Object) : void {
			//TODO: copy the attributes from source and set them on dest
			if (source == null || dest == null) {
				return;
			}
			var description:XML = flash.utils.describeType(dest);
			
			for each(var obj:Object in description..accessor.@name) {
				var attr : String = obj.toString();
				
				dest[attr]=source[attr];
			}			
		}
	}
}